"""

.. module:: spaces

*Description*:

This module contains all Spaces related classes and definitions.

*Constants*:

.. seealso:: class :class:`datastore.resource.Resource`, :class:`datastore.uri.Uri`
"""

import copy
from threading import RLock

from datastore.resource import *
from datastore.uri import *

class Spaces(object):
    """
    A collection of Resources with a Uri as the key.
    
    Generally initialised once when the node is started.
    
    Note: a copy of a resource is placed in spaces and a copy is returned.
    This is ensure that changes to a resource in one service doesn't
    inadvertently change a resource being used by another service.
    
    self.resource is a dictionary with the full uri string as the key and 
    the resource as the value.
    
    self.process is a dictionary with with a full uri string as the key and 
    a callback function as the value.
    
    self.watch is a dictionary with with a full uri string as the key and 
    a tuple (callback function, filter) as the value.
    
    Several methods have a copy parameter. If True, this makes a copy of
    the resource before performing the action in order to reduce the
    chance that modifications to the resource are unintentionally passed 
    between services that use spaces. It may be overridden to handle very
    large resources and resources that contain Python object references.
    
    Trace support creates INFO log messages when a uri matches a trace Uri.
    
    **Constants**:
    
    .. data:: ACTIONS
        
        ACTIONS is a list of names of all possible Spaces actions.

    :param warnings: Check if metadata has definition resources.
    """

    ACTION_GET = 'get'
    ACTION_PUT = 'put'
    ACTION_UPDATE = 'update'
    ACTION_DELETE = 'delete'
    ACTION_REMOVE = 'remove'
    ACTION_FIND = 'find'
    ACTION_EXISTS = 'exists'
    ACTIONS = [ACTION_EXISTS, ACTION_GET, ACTION_PUT, ACTION_UPDATE, ACTION_DELETE, ACTION_REMOVE, ACTION_FIND]

    def __init__(self, warnings=True):
        # Constructor.
        #

        self.logger = logging.getLogger('yarra.datastore.spaces')
        
        # dictionaries for resources, processes and watches. 
        # key is always full Uri.
        self.resource = {}
        self.process = {}
        self.watch = {}
        self.trace = {}
        self.warnings = warnings
        self.counter_lock = RLock()
        # starting with a large number means that string sorting will always
        # work.
        self.counter = 1000000000

    def _get_next_counter(self):
        """
        This function will return a string of a number that is larger than
        for any previous call. This can be used to created sorted collections
        or queues in spaces.
        
        :rtype: A string, suitable for directly appending to a Uri.
        """
        try:
            self.counter_lock.acquire()
            next = self.counter = self.counter + 1
        finally:
            self.counter_lock.release()
        return str(next)
    
    def exists(self, uri):
        """
        Check if a uri exists in spaces.
        Process is invoked with an 'exists' action. Watch is not invoked.
        
        :param uri: Uri to check in spaces.
        :rtype: Boolean - True if it exists, false otherwise. None if uri=None.
        """
        if uri is None:
            return None
        if type(uri) != type(Uri()):
            raise ValueError("URI (%s) must be of type Uri." % uri)
        if uri in self.trace:
            self.logger.log(logging.WARNING, "Trace exists (%s)", uri)
        if uri.full() in self.resource.keys():
            return True
        else:
            return bool(self._process_process(uri, 'exists'))
        
    def get(self, uri, copy=True):
        """
        Return the resource(s) associated with the specified uri or none if
        not found or if the node has no resource.
        
        The returned resource may be in spaces or created by a process callback.
        If the resource is in spaces then it is returned and the process 
        callback is not called. The put method spaces=True will force the 
        resource to be saved in spaces even if there is a process.
        
        :param uri: A uri to fetch (from a process or from spaces itself).
        :param copy: A copy of the resource (default) is added for safety.
        :rtype: A Resource (copy by default) or None.
        """
        if type(uri) != type(Uri()):
            raise ValueError("URI (%s) must be of type Uri." % uri)
        if uri in self.trace:
            self.logger.log(logging.WARNING, "Trace get (%s)", uri)
        res = None
        if uri.full() in self.resource.keys():
            res = self.resource[uri.full()]
        else:
            res = self._process_process(uri, 'get')
        if res is not None:
            self._process_watch(uri, 'get', res)
            if copy:
                return res.copy()
            else:
                return res
        else:
            return None

    def get_collection(self, uri):
        """
        Return the resource(s) associated with the specified uri collection
        or none if not a collection, not found or if the node has no resource.
        
        The returned resource may be in spaces or created by a process Callback.
        
        :param uri: A uri to fetch (from a process or from spaces itself).
        :rtype: List of Resource Uri's or None.
        """
        if type(uri) != type(Uri()):
            raise ValueError("URI (%s) must be of type Uri." % uri)
        if uri in self.trace:
            self.logger.log(logging.WARNING, "Trace get_collection (%s)", uri)
        if uri.is_collection():
            res_list = None
            res_list = []
            for u in sorted(self._find_next_level(self.resource, uri)):
                res_list.append(u)
            return sorted(res_list)
        else:   
            return None

    def get_res_part(self, uri):
        """
        This function fetches a resource and then, if a fragment is specified,
        extracts that part from the resource.
        
        Note: A shallow copy is returned. This could be a problem for complex 
        structures that are shared between multiple services.
        
        :param uri: Uri.
        :rtype: A *copy* of the Resource or Python variable as returned by 
            res.get_var_part()
        """
        if type(uri) != type(Uri()):
            raise ValueError("URI (%s) must be of type Uri." % uri)
        res = self.get(uri.uri())
        if res is None:
            return None
        else:
            return copy.copy(res.get_var_part(uri.fragment()))

    def put(self, uri, resource, warning=True, copy=True, spaces=False):
        """
        Save a resource in spaces using uri as its key. A process Callback may
        place the resource in spaces or somewhere else.
        
        The process may change the uri to something else. This is used mainly 
        when a resource is saved to a collection.
        
        When a put is performed to a collection, the uri used is the collection 
        with the next counter value appended. No assumptions should be made
        about how this counter operates, other than it will always sort higher
        than a previously obtained value.

        :param uri: A Uri to save in spaces.
        :param resource: The resource to be *copied* and saved.
        :param warning: Turn off warnings (boolean) for this put.
        :param copy: A copy of the resource (default) is used for safety.
        :param spaces: Boolean - If True, any process is ignored and the 
            resource is written to spaces.
        :rtype: Uri.
        """
        if copy:
            resource = resource.copy()
        if type(uri) != type(Uri()):
            raise ValueError("URI (%s) must be of type Uri." % uri)
        if uri in self.trace:
            self.logger.log(logging.WARNING, "Trace put (%s)", uri)
        if warning:
            if self.warnings:
                resource.validate(self)
        if self.exists(uri):
            action = 'update'
        else:
            # doesn't exist or uri is a collection
            action = 'put'
        if spaces:
            new_uri = None
        else:
            new_uri = self._process_process(uri, action, resource)
        if new_uri is None:
            if uri.is_collection():
                action = 'put'
                new_uri = uri + self._get_next_counter()
            else:
                new_uri = uri
            self.resource[new_uri.full()] = resource
        self._process_watch(new_uri, action, resource)
        return new_uri

    def request(self, uri, resource, callback, resp='response', 
                suffix=None, filter=['put', 'update'], copy=True):
        """
        This is a convenience method that performs a put to a collection
        and then a watch for a response resource before the put watch is called.
        
        The callback would normally remove the response and delete the request.

        :param uri: A Uri to save in spaces.
        :param resource: The resource to be *copied* and saved.
        :param callback: The watch callback.
        :param resp: The response suffix (default='response')
        :param suffix: The suffix appended to the uri.
        :param filter: The watch filter (default=['put', 'update'])
        :param copy: A copy of the resource (default) is fetched for safety.
        :rtype: Uri - of the request.
        """
        if copy:
            resource = resource.copy()
        if uri in self.trace:
            self.logger.log(logging.WARNING, "Trace request (%s)", uri)
        if uri.is_collection():
            if suffix is None:
                req_uri = uri + self._get_next_counter()
            else:
                req_uri = uri + suffix
            self.set_watch(req_uri + resp, callback, filter=filter)
            self.put(req_uri, resource)
        else:
            raise TypeError("Uri (%s) must be a collection Uri." % uri)
        return req_uri
    
    def delete(self, uri):
        """
        Delete a resource in spaces using uri as its key. A process Callback may
        delete the resource from spaces or somewhere else.
        
        The uri must exist, otherwise an Exception will be raised.
                
        :param uri: A uri to delete.
        :rtype: Nothing is returned.
        """
        if type(uri) != type(Uri()):
            raise ValueError("URI (%s) must be of type Uri." % uri)
        if uri in self.trace:
            self.logger.log(logging.WARNING, "Trace delete (%s)", uri)
        full_uri = uri.full()
        if full_uri in self.resource.keys():
            self._process_watch(uri, 'delete')
            # delete is after watch so we can inspect the resource if needed.
            del self.resource[full_uri]
        else:
            if self._process_process(uri, 'delete'):
                # returns True if something was deleted, 
                # therefore fire the watch.
                self._process_watch(uri, 'delete')
        return None

    def remove(self, uri):
        """
        Returns a resource and then deletes it in spaces using uri as its key. 
        A process Callback may delete the resource from spaces or somewhere 
        else.
        
        The uri must exist, otherwise an Exception will be raised.
        
        :param uri: A uri to remove.
        :rtype: The deleted Resource is returned.
        """
        if type(uri) != type(Uri()):
            raise ValueError("URI (%s) must be of type Uri." % uri)
        if uri in self.trace:
            self.logger.log(logging.WARNING, "Trace remove (%s)", uri)
        full_uri = uri.full()
        if full_uri in self.resource.keys():
            resource = self.resource[full_uri]
            del self.resource[full_uri]
        else:
            resource = self._process_process(uri, 'remove')
        self._process_watch(uri, 'remove', resource)
        return resource

    def find(self, uri):
        """
        Returns a sorted list of uri's that match the specified uri, but does
        not include the uri.
        
        Unlike other methods where the actual contents of spaces is used in
        preference to process generated resources, find is first determined
        using process, and if it returns None, then by a search of spaces.
        Process may return None, [], or some list of uri's.
        
        :param uri: A uri to search for.
        :rtype: A sorted list of shortened uri's.
        """
        if type(uri) != type(Uri()):
            raise ValueError("URI (%s) must be of type Uri." % uri)
        if uri in self.trace:
            self.logger.log(logging.WARNING, "Trace find (%s)", uri)
        uri_list = self._process_process(uri, 'find')
        if uri_list is None:
            return sorted(self._find_all(self.resource, uri))
        else:
            return sorted(uri_list)

    def _process_process(self, uri, action, resource=None):
        """
        Internal method.
        Support virtual resources in spaces. When a request is made to exists, 
        get, put, delete or find resource in spaces, AND the resource isn't 
        physically in spaces, the process Callback is invoked to perform the 
        request.
        
        Only the most specific process is run, that is, the last element of 
        the sorted list of process entries.
        
        A resource is provided to the callback for 'put'. Otherwise it is None. 
        
        A common use for process is where the resource is stored in the
        datastore and is only fetched when needed because of its size.
        
        'update' and 'remove' allows a callback to optimise its behaviour, for
        example, 'delete' doesn't need to fetch the resource, where 'remove'
        does.
        
        :param uri: The Uri to process.
        :param action: A string that is 'exists', 'get', 'put', 'update', 
                'delete', 'remove', or 'find'.
        :param resource: A resource is provided for 'put'.
        :rtype: For process action:
                'exists' -> Boolean or None (for collection).
                'get'    -> A Resource or list of resources for a collection.
                'put'    -> The new Uri.
                'update' -> The new Uri.
                'delete' -> True if Resource deleted, None or False otherwise.
                'remove' -> The resource that was just deleted.
                'find'   -> A list of Uri's.
        """
        process_list = []
        for u in sorted(self._search(self.process, uri)):
            # each entry in process is a list of callbacks.
            process_list += self.process[u.full()]
        if len(process_list) > 0:
            # only execute the last process.
            callback = process_list[-1]
            if type(callback) in [types.FunctionType,types.MethodType]:
                try:
                    return callback(uri, action, resource)
                except:
                    self.logger.exception(
                       "Unable to execute process callback (%s)",
                       sys.exc_info()[1])
            else:
                raise TypeError("Callback expecting FunctionType " + \
                    "or MethodType, not %s." % type(callback))
    
    def set_process(self, uri, callback):
        """
        :param uri: The Uri to process. The Uri is tested at each split 
            (on slash), with and without a trailing slash.
        :param callback: Callback to invoke if the process uri is matched.
        """
        if type(uri) != type(Uri()):
            raise ValueError("URI (%s) must be of type Uri." % uri)
        if uri in self.trace:
            self.logger.log(logging.WARNING, "Trace set_process (%s)", uri)
        if uri.full() in self.process.keys():
            if not callback in self.process[uri.full()]:
                self.process[uri.full()].append(callback)
        else:
            self.process[uri.full()] = [callback]
        return None
    
    def del_process(self, uri, callback):
        """
        :param uri: Uri
        :param callback: method
        """
        if type(uri) != type(Uri()):
            raise ValueError("URI (%s) must be of type Uri." % uri)
        if uri in self.trace:
            self.logger.log(logging.WARNING, "Trace del_process (%s)", uri)
        for cb in self.process[uri.full()]:
            if cb == callback:
                self.process[uri.full()].remove(callback)
        return None

    def _process_watch(self, uri, action, resource=None):
        """
        Internal method.
        
        Allow actions in spaces to be monitored by one or more callback's.
        
        'get', put', 'update', 'delete' or 'remove' can be monitored. 
        'find' and 'exists' cannot be monitored.
        
        The Callback actions may perform additional processing that creates
        appropriate side-effects for the action.
        
        Watch cannot change what is returned by the spaces request.
        
        :param uri: The Uri to process.
        :param action: A string that is 'get', put', 'update', 'delete', 
            or 'remove'.
        :param resource: A resource or list is provided for 'get', 'put'.
        :rtype: Nothing is returned.
        """
        watch_list = []
        match_uri_list = self._search(self.watch, uri)
        match_uri_list.sort()
        for u in match_uri_list:
            # each entry in watch is a list of callbacks.
            for c in self.watch[u]:
                watch_list.append(c)
        for callback, filter in watch_list:
            if action in filter:
                if callback.__class__ in [types.FunctionType, types.MethodType]:
                    try:
                        callback(uri, action, resource)
                    except:
                        self.logger.exception(
                            "Unable to execute watch callback (%s)",
                                  sys.exc_info()[1])
                else:
                    raise TypeError("Callback expecting FunctionType " + \
                        "or MethodType, not %s." % type(callback))
        return None
 
    def get_watch(self, callback):
        """
        :param callback: method
        :rtype: list of Uri's that match the specified callback function/method.
        """ 
        uri_list = []
        if type(callback) in [types.FunctionType,types.MethodType]:
            for watch_uri in self.watch.keys():
                for w in self.watch[watch_uri]:
                    if str(callback) == str(w[0]):
                        uri_list.append(watch_uri)
            return uri_list
        else:
            raise ValueError(
            "Callback (%s) must be a Python function or method reference." % \
            type(callback))

    def set_watch(self, uri, callback, filter=ACTIONS):
        """
        :param uri: The Uri to watch. The Uri is tested at each split 
            (on slash), with and without a trailing slash.
        :param callback: Callback to invoke if the watch uri is matched.
        :param filter: 
        """
        if type(uri) != type(Uri()):
            raise ValueError("URI (%s) must be of type Uri." % uri)
        if uri in self.trace:
            self.logger.log(logging.WARNING, "Trace set_watch (%s)", uri)
        if uri.full() in self.watch.keys():
            self.watch[uri.full()].append((callback, filter))
        else:
            self.watch[uri.full()] = [(callback, filter)]
        return None

    def del_watch(self, uri, callback):
        """
        :param uri: Uri
        :param callback: method
        """
        if type(uri) != type(Uri()):
            raise ValueError("URI (%s) must be of type Uri." % uri)
        if uri in self.trace:
            self.logger.log(logging.WARNING, "Trace del_watch (%s)", uri)
        for cb, filter in self.watch[uri.full()]:
            if cb == callback:
                self.watch[uri.full()].remove((callback, filter))
        return None
    
    def set_trace(self, uri):
        """
        :param uri: Uri
        """
        self.trace[uri] = True
        self.logger.log(logging.WARNING, "Trace set (%s)", uri)

    def del_trace(self, uri):
        """
        :param uri: Uri
        """
        if uri in self.trace:
            del self.trace[uri]
            self.logger.log(logging.WARNING, "Trace deleted (%s)", uri)

    def _search(self, dict, uri):
        """
        An internal method that returns a list of uri's found in the
        dict that match all or part of the specified uri.
        
        The matching is done on a split uri (split on slash except for a urn)
        with and without the split character.
        
        The list is *not* sorted.
        
        :param dict: Usually self.process or self.watch.
        :param uri: Uri to search.
        :rtype: List of matching uri's.
        """
        parts = uri.split()
        if parts is None:
            parts = []
        bit = ''
        uri_list = []
        for p in parts:
            bit += p
            bit_uri = Uri(bit)
            if bit_uri in dict.keys():
                # with trailing slash
                uri_list.append(bit_uri)
            elif bit_uri.collection(False) in dict.keys():
                # without trailing slash 
                uri_list.append(bit_uri.collection(False))
        return uri_list
        
    def _find_all(self, dict, uri):
        """
        An internal method that returns a dict of uri's found in the
        dict that starts with, but does not include, the specified uri.
        
        The list is *not* sorted.
        
        The cost of this function is always order n. It trades-off simplicity 
        for the extra performance available if a tree was used to store the
        uri's.
        
        If a null Uri is specified, return the entire dictionary.
        
        :param dict: Any dictionary that has uri's as the key.
        :param uri: Uri to search.
        :rtype: List of matching uri's.
        """
        if len(uri) == 0:
            return dict.keys()
        full_uri = uri.full()
        uri_list = []
        for uri_key in dict.keys():
            if uri_key != full_uri:
                if str(uri_key).startswith(str(full_uri)):
                    uri_list.append(uri_key.shorten())
        return uri_list

    def _find_next_level(self, dict, uri):
        """
        An internal method that returns a dict of uri's found in the
        dict that are at the next level compared to the specified uri. That is,
        the matching uri has at least one additional level.
        
        The list is *not* sorted and does not include the 'uri' parameter value.
        
        :param dict: Any dictionary that has uri's as the key.
        :param uri: Uri to search.
        :rtype: List of matching uri's (expanded full_uri).
        """
        full_uri = uri.full()
        uri_list = []
        levels = len(uri.split()) + 1
        for uri_key in dict.keys():
            if uri_key != full_uri:
                if str(uri_key).startswith(str(full_uri)):
                    if len(Uri(uri_key).split()) == levels:
                        uri_list.append(Uri(uri_key))
        return uri_list
